package com.tegistuff.webos.run.debugger;

import com.intellij.execution.ExecutionException;
import com.intellij.openapi.ui.MessageType;
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.breakpoints.XBreakpointProperties;
import com.intellij.xdebugger.breakpoints.XLineBreakpoint;
import com.intellij.xdebugger.evaluation.XDebuggerEvaluator;
import com.palm.webos.tools.internal.debugger.AppDebugSession;
import com.palm.webos.tools.internal.debugger.Dispatcher;
import com.tegistuff.webos.run.WebOSRunConfiguration;
import com.tegistuff.webos.run.debugger.breakpoints.WebOSBreakpointWrapper;
import com.tegistuff.webos.run.debugger.commands.*;
import com.tegistuff.webos.run.debugger.infrastructure.WebOSCommandSpooler;
import com.tegistuff.webos.run.debugger.infrastructure.WebOSDebuggerConstants;
import com.tegistuff.webos.run.debugger.infrastructure.WebOSVar;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * WebOSDebuggerProxy.java
 *
 * Copyright (C) 2011-2012 Timo Tegtmeier.
 * All rights reserved.
 */
public class WebOSDebuggerProxy implements AppDebugSession.SessionListener, Dispatcher.EventListener {

    private WebOSRunConfiguration myRunConfiguration;
    private WebOSDebugProcess myDebugProcess;
    private XDebugSession myDebugSession;
    private WebOSDebugSessionProvider myDebugSessionProvider;
    private AppDebugSession myAppDebugSession;
    private WebOSCommandSpooler myCommandSpooler;
    private boolean myIsRunning;

    public WebOSDebuggerProxy(WebOSRunConfiguration configuration) {
        myRunConfiguration = configuration;
    }

    public void setDebugProcess(WebOSDebugProcess debugProcess) {
        myDebugProcess = debugProcess;
        myDebugSession = debugProcess.getSession();
        myCommandSpooler = new WebOSCommandSpooler(myDebugProcess);
        myCommandSpooler.runCommand(new WebOSSetExceptionBreakCommand(true));
    }

    public void connect() throws ExecutionException {
        myDebugSessionProvider = new WebOSDebugSessionProvider(myRunConfiguration,
                myRunConfiguration.getAppId(), myRunConfiguration.V8DebugPort);
        doConnect();
    }

    private void doConnect() throws ExecutionException {
        for(int i = 1; i <= WebOSDebuggerConstants.MAX_RETRIES; i++) {
            try {
                myAppDebugSession = myDebugSessionProvider.connect();
                myAppDebugSession.addSessionListener(this);
                myAppDebugSession.addEventListener(this);
                break;
            } catch(Exception e) {
                if(i >= WebOSDebuggerConstants.MAX_RETRIES) {
                    throw new ExecutionException("Unable to connect to debugger: " + e.getMessage());
                }
            }
        }
        myCommandSpooler.onDebuggerConnected();
    }

    public final void continueExecution() {
        myCommandSpooler.runCommand(new WebOSContinueCommand());
    }

    public final void stop() {
        myCommandSpooler.clear();
        if(!isReady())
            return;

        myCommandSpooler.runCommand(new WebOSSetExceptionBreakCommand(false));
        myDebugProcess.getLineBreakpointHandler().finalizeBreakpoints();
        if(!this.myIsRunning)
            myCommandSpooler.runCommand(new WebOSContinueCommand());
        myCommandSpooler.runCommand(new WebOSDisconnectCommand(myDebugSessionProvider));
    }

    public final void setLineBreakpoint(XLineBreakpoint<XBreakpointProperties> breakpoint, String fileName, int lineNumber) {
        myCommandSpooler.runCommand(new WebOSSetBreakpointCommand(breakpoint, fileName, lineNumber));
    }
    
    public final void removeLineBreakpoint(WebOSBreakpointWrapper wrapper) {
        myCommandSpooler.runCommand(new WebOSRemoveBreakpointCommand(wrapper));
    }
    
    public final void setLineBreakpointState(WebOSBreakpointWrapper wrapper, boolean enabled) {
        myCommandSpooler.runCommand(new WebOSChangeBreakpointCommand(wrapper, enabled));
    }

    public final void stepInto() {
        myCommandSpooler.runCommand(new WebOSStepInCommand());
    }

    public final void stepOver() {
        myCommandSpooler.runCommand(new WebOSStepOverCommand());
    }

    public final void stepOut() {
        myCommandSpooler.runCommand(new WebOSStepOutCommand());
    }
    
    public final void evaluate(String expression, XDebuggerEvaluator.XEvaluationCallback callback) {
        WebOSStackFrame frame = (WebOSStackFrame)myDebugSession.getCurrentStackFrame();
        WebOSEvaluateCommand command = new WebOSEvaluateCommand(expression, frame.getFrameNumber());
        try {
            myCommandSpooler.runCommand(command).waitForCompletion();
            JSONObject result = command.getResult();
            if(result != null)
                callback.evaluated(new WebOSVar(myDebugProcess, command.getResult()));
        } catch (JSONException e) {
            callback.errorOccurred("Error evaluating JSON data.");
        } catch (InterruptedException e) {
            callback.errorOccurred("Debugger thread has been interrupted.");
        }
    }

    /***
     * Called when the session has been terminated by the debugger.
     */
    @Override
    public void sessionTerminated() {
        if(!myDebugSession.isStopped() && !myDebugProcess.isClosing()) {
            myDebugSession.reportMessage("Debugger session has unexpectedly been terminated.", MessageType.WARNING);
            myDebugSession.stop();
        }
    }

    /***
     * Called when an event (most commonly a "break") has occurred.
     * @param event     JSON event object
     */
    @Override
    public void eventReceived(JSONObject event) {
        try {
            String type = event.getString("event");
            setSuspended();
            if(type.equals("break")) {
                handleBreak(event);
            } else if(type.equals("exception")) {
                handleException(event);
            }
        } catch(Exception e) {
            myDebugSession.reportError("Failed to handle debugger event: " + e.getMessage());
        }
    }

    private final void handleBreak(JSONObject event) {
        myCommandSpooler.runCommand(new WebOSBuildExecutionStackCommand(false));
    }

    private final void handleException(JSONObject event) {
        try {
            String exceptionText = event.getJSONObject("body").getJSONObject("exception").getString("text");
            myDebugSession.reportMessage("Execution suspended due to uncaught exception: " + exceptionText,
                    MessageType.INFO);
        } catch(JSONException e) {
            myDebugSession.reportMessage("Execution suspended due to uncaught exception", MessageType.INFO);
        }
        myCommandSpooler.runCommand(new WebOSBuildExecutionStackCommand(true));
    }

    public final AppDebugSession getAppDebugSession() {
        return myAppDebugSession;
    }

    public final WebOSCommandSpooler getCommandSpooler() {
        return myCommandSpooler;
    }

    public final boolean isReady() {
        return (myDebugSessionProvider != null) && myDebugSessionProvider.isReady();
    }

    public final void setRunning() {
        myIsRunning = true;
    }

    public final void setSuspended() {
        myIsRunning = false;
    }
}
